package atom.util {
	
	/**
	 * Array based Collection manager.
	 * @author Jorge Braccini
	 */
	public class Collection {
		
		private var _collection:Array;
		
		/**
		 * Creates a new collection populated with the specified values
		 * @param	... values
		 */
		public function Collection(... values) {
			_collection = [];
			addItems(values);
		}
		
		/**
		 * Adds the specified items to the collection
		 * @param	... items
		 */
		public function addItems(... items):void {
			for each (var item:* in items) {
				if (item is Array) {
					for each (var subItem:* in item) {
						addItems(subItem);
					}
				} else if (item is Collection) {
					for each (subItem in item.itemList) {
						addItems(subItem);
					}
				} else {
					if (!contains(item)) {
						_collection.push(item);
					}
				}
			}
		}
		
		/**
		 * Removes all of the specified items from the collection
		 * @param	... items
		 */
		public function removeItems(... items):void {
			for each (var item:* in items) {
				if (item is Array) {
					for each (var subItem:* in item) {
						removeItems(subItem);
					}
				} else if (item is Collection) {
					for each (subItem in item.itemList) {
						removeItems(subItem);
					}
				} else if (contains(item)) {
					_collection.splice(_collection.indexOf(item), 1);
				}
			}
		}
		
		/**
		 * Returns true iff collection contains (or is) specified item
		 * @param	item
		 * @return
		 */
		public function contains(item:*):Boolean {	
			if ((item is Array) || (item is Collection)) {
				return containsAll(item);
			} else {
				if ((_collection.indexOf(item) > -1) || (this === item)) {
					return true;
				} else {
					return false;
				}
			}
		}
		
		/**
		 * Returns true iff collection contains all specified items (including subitems of an array or collection)
		 * @param	... items
		 * @return
		 */
		public function containsAll(... items):Boolean {	
			var all:Boolean = true;
			for each (var item:* in items) {
				if (item is Array) {
					for each (var subItem:* in item) {
						if (!_collection.containsAll(subItem)) { all = false; }
					}
				} else if (item is Collection) {
					for each (subItem in item.itemList) {
						if (!_collection.containsAll(subItem)) { all = false; }
					}
				} else {	
					if (!_collection.contains(item)) { all = false; }
				}
			}
			return all;
		}
		
		/**
		 * Returns true iff collection contains any of the specified items (including subitems of an array or collection)
		 * @param	... items
		 * @return
		 */
		public function containsAny(... items):Boolean {	
			var any:Boolean = false;
			for each (var item:* in items) {
				if (item is Array) {
					for each (var subItem:* in item) {
						if (!_collection.containsAny(subItem)) { 
							any = true;
							break;
						}
					}
				} else if (item is Collection) {
					for each (subItem in item.itemList) {
						if (!_collection.containsAny(subItem)) { 
							any = true; 
							break;
						}
					}
				} else {	
					if (!_collection.contains(item)) { 
						any = true;
						break;
					}
				}
			}
			return any;
		}
		
		/**
		 * Executes a test function on each item in the collection and constructs a new collection of all items that return true.
		 * @param	callback
		 * @param	thisObject
		 * @return
		 */
		public function filter(callback:Function, thisObject:* = null):Collection {
			var filtered:Collection = new Collection();
			filtered.addItems(_collection.filter(callback, thisObject));
			return filtered;
		}
		
		/**
		 * Executes a function on each item in the collection.
		 * @param	callback
		 * @param	thisObject
		 */
		public function forEach(callback:Function, thisObject:* = null):void {
			_collection.forEach(callback, thisObject);
		}
		
		/**
		 * Converts the elements in a collection to strings, inserts the specified separator between the elements, concatenates them, and returns the resulting string.
		 * @param	sep
		 * @return
		 */
		public function join(sep:*):String { 
			return _collection.join(sep);
		}
		
		/**
		 * Executes a function on each item in an collection, and constructs a new collection of items corresponding to the results of the function on each item in the original collection.
		 * @param	callback
		 * @param	thisObject
		 * @return
		 */
		public function map(callback:Function, thisObject:* = null):Collection {
			var mappedArray:Array = _collection.map(callback, thisObject);
			var mappedCollection:Collection = new Collection();
			for each (var item:* in mappedArray) {
				mappedCollection.addItems(item);
			}
			return mappedCollection;
		}
		
		/**
		 * Executes a test function on each item in the collection until an item is reached that returns true. Use this method to determine whether any items in a collection meet a criterion, such as having a value less than a particular number.
		 * @param	callback
		 * @param	thisObject
		 * @return
		 */
		public function some(callback:Function, thisObject:* = null):Boolean {
			return _collection.some(callback, thisObject);
		}
		
		/**
		 * Executes a test function on each item in the collection until an item is reached that returns false for the specified function. You use this method to determine whether all items in a collection meet a criterion, such as having values less than a particular number. 
		 * @param	callback
		 * @param	thisObject
		 * @return
		 */
		public function every(callback:Function, thisObject:* = null):Boolean {
			return _collection.every(callback, thisObject);
		}
		
		/**
		 * Returns a collection made up of items in this collection for which item.property==value
		 * @param	property
		 * @param	value
		 * @return
		 */
		public function subCollection(property:String, value:*):Collection { 
			///Ideally this would accept a method name and a ...params and check to see if item.method(...params)==true
			var subCollection:Collection = new Collection();
			for each (var item:* in _collection) {
				try {
					if (item[property]==value) {
						subCollection.addItems(item);
					}	
				} catch (err:Error) {
					//this is likely to be very very error prone if the programmer isn't careful so we'll just break out of the loop
					break;
				}
			}
			return subCollection;
		}
		
		/**
		 * Returns a collection made up of all items that are in both this collection and the specified collection
		 * @param	coll
		 * @return
		 */
		public function intersection(coll:Collection):Collection {
			var intersectColl:Collection=new Collection();
			for each (var item:* in coll.itemList) {
				if (this.contains(item)) { 
					intersectColl.addItems(item);
				}
			}
			return intersectColl;
		}
		
		/**
		 * Returns the intersection of this collection with an arbitrary number of other collections
		 * @param	... colls
		 * @return
		 */
		public function intersectMany(... colls):Collection {
			var intersectColl:Collection = new Collection();
			intersectColl.addItems(_collection);
			for each (var subItem:* in colls) {
				if (subItem is Collection) {
					intersectColl = intersectColl.intersection(subItem);
				}
			}
			return intersectColl;
		}
		
		/**
		 * Returns a collection made up of all items that are in either this collection or the specified collection (or both)
		 * @param	coll
		 * @return
		 */
		public function union(coll:Collection):Collection {
			var unionColl:Collection = new Collection();
			unionColl.addItems(itemList, coll);
			return unionColl;
		}
		
		/**
		 * Returns the union of this collection with an arbitrary number of other collections
		 * @param	... colls
		 * @return
		 */
		public function unionMany(... colls):Collection {
			var unionColl:Collection = new Collection();
			unionColl.addItems(_collection);
			for each (var subItem:* in colls) {
				if (subItem is Collection) {
					unionColl = unionColl.union(subItem);
				}
			}
			return unionColl;
		}
		
		/**
		 * Returns the relative complement of the specified collection in this collection
		 * A.relComp(B) := A\B
		 * @param	coll
		 * @return
		 */
		public function relComp(coll:Collection):Collection {
			///Returns the relative complement of the specified collection in this collection
			///A.relComp(B) := A\B
			///i.e. returns a collection containing items that are in this collection but not in the specified collection
			var rcColl:Collection = new Collection();
			rcColl.addItems(_collection);
			rcColl.removeItems(coll);
			return rcColl;
		}
		
		/**
		 * Returns the relative complement of the union of the specified collections in this collection
		 * @param	... colls
		 * @return
		 */
		public function relCompMany(... colls):Collection {
			var rcColl:Collection = new Collection();
			rcColl.addItems(_collection);
			for each (var subItem:* in colls) {
				if (subItem is Collection) {
					rcColl.removeItems(subItem);
				}
			}
			return rcColl;
		}

		/**
		 * Returns the number of items in the collection
		 */
		public function get numItems():uint {
			return _collection.length;
		}
		
		/**
		 * Exposes the underlying array; use this only for e.g. for-each loops
		 */
		public function get itemList():Array {
			return _collection;
		}
	}
}